/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.util;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * Wrapper for a {@code byte[]}, which provides read-only access and can
 * "reveal" a partial slice of the underlying array. <b>Note:</b> Multibyte
 * accessors all use big-endian order.
 */
public final class ByteArray {

	/** {@code non-null;} underlying array */
	private final byte[] bytes;

	/** {@code >= 0}; start index of the slice (inclusive) */
	private final int start;

	/**
	 * {@code >= 0, <= bytes.length}; size computed as {@code end - start} (in
	 * the constructor)
	 */
	private final int size;

	/**
	 * Constructs an instance.
	 * 
	 * @param bytes
	 *            {@code non-null;} the underlying array
	 * @param start
	 *            {@code >= 0;} start index of the slice (inclusive)
	 * @param end
	 *            {@code >= start, <= bytes.length;} end index of the slice
	 *            (exclusive)
	 */
	public ByteArray(byte[] bytes, int start, int end) {
		if (bytes == null) {
			throw new NullPointerException("bytes == null");
		}

		if (start < 0) {
			throw new IllegalArgumentException("start < 0");
		}

		if (end < start) {
			throw new IllegalArgumentException("end < start");
		}

		if (end > bytes.length) {
			throw new IllegalArgumentException("end > bytes.length");
		}

		this.bytes = bytes;
		this.start = start;
		this.size = end - start;
	}

	/**
	 * Constructs an instance from an entire {@code byte[]}.
	 * 
	 * @param bytes
	 *            {@code non-null;} the underlying array
	 */
	public ByteArray(byte[] bytes) {
		this(bytes, 0, bytes.length);
	}

	/**
	 * Gets the size of the array, in bytes.
	 * 
	 * @return {@code >= 0;} the size
	 */
	public int size() {
		return size;
	}

	/**
	 * Returns a slice (that is, a sub-array) of this instance.
	 * 
	 * @param start
	 *            {@code >= 0;} start index of the slice (inclusive)
	 * @param end
	 *            {@code >= start, <= size();} end index of the slice
	 *            (exclusive)
	 * @return {@code non-null;} the slice
	 */
	public ByteArray slice(int start, int end) {
		checkOffsets(start, end);
		return new ByteArray(bytes, start + this.start, end + this.start);
	}

	/**
	 * Returns the offset into the given array represented by the given offset
	 * into this instance.
	 * 
	 * @param offset
	 *            offset into this instance
	 * @param bytes
	 *            {@code non-null;} (alleged) underlying array
	 * @return corresponding offset into {@code bytes}
	 * @throws IllegalArgumentException
	 *             thrown if {@code bytes} is not the underlying array of this
	 *             instance
	 */
	public int underlyingOffset(int offset, byte[] bytes) {
		if (bytes != this.bytes) {
			throw new IllegalArgumentException("wrong bytes");
		}

		return start + offset;
	}

	/**
	 * Gets the {@code signed byte} value at a particular offset.
	 * 
	 * @param off
	 *            {@code >= 0, < size();} offset to fetch
	 * @return {@code signed byte} at that offset
	 */
	public int getByte(int off) {
		checkOffsets(off, off + 1);
		return getByte0(off);
	}

	/**
	 * Gets the {@code signed short} value at a particular offset.
	 * 
	 * @param off
	 *            {@code >= 0, < (size() - 1);} offset to fetch
	 * @return {@code signed short} at that offset
	 */
	public int getShort(int off) {
		checkOffsets(off, off + 2);
		return (getByte0(off) << 8) | getUnsignedByte0(off + 1);
	}

	/**
	 * Gets the {@code signed int} value at a particular offset.
	 * 
	 * @param off
	 *            {@code >= 0, < (size() - 3);} offset to fetch
	 * @return {@code signed int} at that offset
	 */
	public int getInt(int off) {
		checkOffsets(off, off + 4);
		return (getByte0(off) << 24) | (getUnsignedByte0(off + 1) << 16)
				| (getUnsignedByte0(off + 2) << 8) | getUnsignedByte0(off + 3);
	}

	/**
	 * Gets the {@code signed long} value at a particular offset.
	 * 
	 * @param off
	 *            {@code >= 0, < (size() - 7);} offset to fetch
	 * @return {@code signed int} at that offset
	 */
	public long getLong(int off) {
		checkOffsets(off, off + 8);
		int part1 = (getByte0(off) << 24) | (getUnsignedByte0(off + 1) << 16)
				| (getUnsignedByte0(off + 2) << 8) | getUnsignedByte0(off + 3);
		int part2 = (getByte0(off + 4) << 24)
				| (getUnsignedByte0(off + 5) << 16)
				| (getUnsignedByte0(off + 6) << 8) | getUnsignedByte0(off + 7);

		return (part2 & 0xffffffffL) | ((long) part1) << 32;
	}

	/**
	 * Gets the {@code unsigned byte} value at a particular offset.
	 * 
	 * @param off
	 *            {@code >= 0, < size();} offset to fetch
	 * @return {@code unsigned byte} at that offset
	 */
	public int getUnsignedByte(int off) {
		checkOffsets(off, off + 1);
		return getUnsignedByte0(off);
	}

	/**
	 * Gets the {@code unsigned short} value at a particular offset.
	 * 
	 * @param off
	 *            {@code >= 0, < (size() - 1);} offset to fetch
	 * @return {@code unsigned short} at that offset
	 */
	public int getUnsignedShort(int off) {
		checkOffsets(off, off + 2);
		return (getUnsignedByte0(off) << 8) | getUnsignedByte0(off + 1);
	}

	/**
	 * Copies the contents of this instance into the given raw {@code byte[]} at
	 * the given offset. The given array must be large enough.
	 * 
	 * @param out
	 *            {@code non-null;} array to hold the output
	 * @param offset
	 *            {@code non-null;} index into {@code out} for the first byte of
	 *            output
	 */
	public void getBytes(byte[] out, int offset) {
		if ((out.length - offset) < size) {
			throw new IndexOutOfBoundsException("(out.length - offset) < "
					+ "size()");
		}

		System.arraycopy(bytes, start, out, offset, size);
	}

	/**
	 * Checks a range of offsets for validity, throwing if invalid.
	 * 
	 * @param s
	 *            start offset (inclusive)
	 * @param e
	 *            end offset (exclusive)
	 */
	private void checkOffsets(int s, int e) {
		if ((s < 0) || (e < s) || (e > size)) {
			throw new IllegalArgumentException("bad range: " + s + ".." + e
					+ "; actual size " + size);
		}
	}

	/**
	 * Gets the {@code signed byte} value at the given offset, without doing any
	 * argument checking.
	 * 
	 * @param off
	 *            offset to fetch
	 * @return byte at that offset
	 */
	private int getByte0(int off) {
		return bytes[start + off];
	}

	/**
	 * Gets the {@code unsigned byte} value at the given offset, without doing
	 * any argument checking.
	 * 
	 * @param off
	 *            offset to fetch
	 * @return byte at that offset
	 */
	private int getUnsignedByte0(int off) {
		return bytes[start + off] & 0xff;
	}

	/**
	 * Gets a {@code DataInputStream} that reads from this instance, with the
	 * cursor starting at the beginning of this instance's data. <b>Note:</b>
	 * The returned instance may be cast to {@link #GetCursor} if needed.
	 * 
	 * @return {@code non-null;} an appropriately-constructed
	 *         {@code DataInputStream} instance
	 */
	public MyDataInputStream makeDataInputStream() {
		return new MyDataInputStream(makeInputStream());
	}

	/**
	 * Gets a {@code InputStream} that reads from this instance, with the cursor
	 * starting at the beginning of this instance's data. <b>Note:</b> The
	 * returned instance may be cast to {@link #GetCursor} if needed.
	 * 
	 * @return {@code non-null;} an appropriately-constructed
	 *         {@code InputStream} instancex
	 */
	public MyInputStream makeInputStream() {
		return new MyInputStream();
	}

	/**
	 * Helper interface that allows one to get the cursor (of a stream).
	 */
	public interface GetCursor {

		/**
		 * Gets the current cursor.
		 * 
		 * @return {@code 0..size();} the cursor
		 */
		public int getCursor();
	}

	/**
	 * Helper class for {@link #makeInputStream}, which implements the stream
	 * functionality.
	 */
	public class MyInputStream extends InputStream {

		/** 0..size; the cursor */
		private int cursor;

		/** 0..size; the mark */
		private int mark;

		public MyInputStream() {
			cursor = 0;
			mark = 0;
		}

		public int read() throws IOException {
			if (cursor >= size) {
				return -1;
			}

			int result = getUnsignedByte0(cursor);
			cursor++;
			return result;
		}

		public int read(byte[] arr, int offset, int length) {
			if ((offset + length) > arr.length) {
				length = arr.length - offset;
			}

			int maxLength = size - cursor;
			if (length > maxLength) {
				length = maxLength;
			}

			System.arraycopy(bytes, cursor + start, arr, offset, length);
			cursor += length;
			return length;
		}

		public int available() {
			return size - cursor;
		}

		public void mark(int reserve) {
			mark = cursor;
		}

		public void reset() {
			cursor = mark;
		}

		public boolean markSupported() {
			return true;
		}
	}

	/**
	 * Helper class for {@link #makeDataInputStream}. This is used simply so
	 * that the cursor of a wrapped {@link #MyInputStream} instance may be
	 * easily determined.
	 */
	public static class MyDataInputStream extends DataInputStream {

		/** {@code non-null;} the underlying {@link #MyInputStream} */
		private final MyInputStream wrapped;

		public MyDataInputStream(MyInputStream wrapped) {
			super(wrapped);

			this.wrapped = wrapped;
		}
	}
}
